/*******************************************************************************
 * Copyright (c) 2011 LegSem.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.txt
 * 
 * Contributors:
 *     LegSem - initial API and implementation
 ******************************************************************************/
package com.legsem.legstar.jca;

import java.io.PrintWriter;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;

import javax.resource.ResourceException;
import javax.resource.cci.ConnectionFactory;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.legsem.legstar.LegStarBoundBean;
import com.legsem.legstar.LegStarUtil;
import com.legsem.legstar.cci.LegStarConnectionRequestInfo;
import com.legsem.legstar.cci.LegStarConnectionSpec;
import com.legstar.coxb.host.HostContext;
import com.legstar.messaging.ConnectionException;
import com.legstar.messaging.HostEndpoint;
import com.legstar.messaging.LegStarAddress;

/**
 * An abstract implementation of a Managed Connection Factory to be shared by
 * all LegStar transports.
 * <p/>
 * The application server uses the ManagedConnectionFactory interface
 * (implemented by the resource adapter) to create a new physical connection to
 * the underlying EIS.
 * <p/>
 * ManagedConnectionFactory instance is a factory of both ManagedConnection and
 * EIS-specific connection factory instances. This interface supports connection
 * pooling by providing methods for matching and creation of ManagedConnection
 * instance. A ManagedConnectionFactory instance is required to be a JavaBean.
 */
public abstract class LegStarManagedConnectionFactoryImpl extends
        LegStarBoundBean implements ManagedConnectionFactory {

    private static final long serialVersionUID = -3306904801759008035L;

    /** Logger. */
    private final Log log = LogFactory.getLog(getClass());

    private transient PrintWriter writer;

    // Use recommended Connector terminology for usual properties
    private String serverName;
    private int portNumber;
    private String userName;
    private String password;

    private String hostCharset;

    /** Host trace mode. */
    private boolean hostTraceMode;

    /** Maximum time to wait for connection. */
    private int hostConnectTimeout = HostEndpoint.DEFAULT_CONNECT_TIMEOUT_MSEC;

    /** Maximum time to wait for a reply to a request. */
    private int hostReceiveTimeout = HostEndpoint.DEFAULT_RECEIVE_TIMEOUT_MSEC;

    /**
     * Creates a new physical connection to the underlying EIS resource manager.
     * ManagedConnectionFactory uses the security information (passed as
     * Subject) and additional ConnectionRequestInfo (which is specific to
     * ResourceAdapter and opaque to application server) to create this new
     * connection.
     * 
     * (non-Javadoc)
     * 
     * @see javax.resource.spi.ManagedConnectionFactory#createManagedConnection(javax.security.auth.Subject,
     *      javax.resource.spi.ConnectionRequestInfo)
     */
    public ManagedConnection createManagedConnection(Subject subject,
            ConnectionRequestInfo connectionRequestInfo)
            throws ResourceException {
        if (log.isDebugEnabled()) {
            log.debug("Creating a new Managed Connection for subject: "
                    + LegStarUtil.toString(subject) + ", requestInfo: " + connectionRequestInfo);
        }
        try {
            // Mainframe takes the first 16 characters to correlate traces
            String connectionID = UUID.randomUUID().toString().replace("-", "");

            HostEndpoint hostEndpoint = sanitize(
                    createHostEndpoint(subject, connectionRequestInfo),
                    subject, connectionRequestInfo);

            LegStarAddress address = new LegStarAddress(hostEndpoint);
            com.legstar.messaging.LegStarConnection hostConnection = hostEndpoint
                    .getHostConnectionfactory().createConnection(connectionID,
                            address, hostEndpoint);

            if (log.isDebugEnabled()) {
                log.debug("Created a new LegStar connection: " + connectionID);
            }
            return createManagedConnection(hostConnection, hostEndpoint);

        } catch (ConnectionException e) {
            throw new ResourceException(e);
        }
    }

    /**
     * Make sure contextual parameters override the configured ones and also
     * that sound defaults are provided.
     * <p/>
     * User/Password can be provided by the application server as part of the
     * subject or by the caller as part of the ConnectionRequestInfo. If none of
     * these contains credentials, the defaults taken from the configuration are
     * kept.
     * 
     * @param hostEndpoint the current host endpoint populated from
     *            configuration parameters
     * @param subject JAAS credentials set by the application server
     * @param connectionRequestInfo the request information
     * @return a sanitized, ready to use, host endpoint
     */
    protected HostEndpoint sanitize(HostEndpoint hostEndpoint, Subject subject,
            ConnectionRequestInfo connectionRequestInfo) {

        if (subject != null) {
            PasswordCredential pc = AccessController
                    .doPrivileged(new GetCredentialAction(subject, this));

            if (pc != null) {
                hostEndpoint.setHostUserID(pc.getUserName());
                hostEndpoint.setHostPassword(new String(pc.getPassword()));
            }
        }

        if (connectionRequestInfo != null) {
            LegStarConnectionSpec specs = ((LegStarConnectionRequestInfo) connectionRequestInfo)
                    .getLegStarConnectionSpec();
            if (specs != null) {
                if (specs.getHostUserID() != null) {
                    hostEndpoint.setHostUserID(specs.getHostUserID());
                }
                if (specs.getHostPassword() != null) {
                    hostEndpoint.setHostPassword(specs.getHostPassword());
                }
                hostEndpoint.setHostTraceMode(specs.isHostTraceMode());

            }
        }

        // If no one passed User/password, assume the mainframe is not expecting
        // any. LegStar Transports are not ready for null
        if (hostEndpoint.getHostUserID() == null) {
            hostEndpoint.setHostUserID("");
        }
        if (hostEndpoint.getHostPassword() == null) {
            hostEndpoint.setHostPassword("");
        }

        if (hostEndpoint.getHostCharset() == null) {
            hostEndpoint
                    .setHostCharset(HostContext.getDefaultHostCharsetName());
        }

        if (hostEndpoint.getConnectTimeout() < 1) {
            hostEndpoint
                    .setConnectTimeout(HostEndpoint.DEFAULT_CONNECT_TIMEOUT_MSEC);
        }
        if (hostEndpoint.getReceiveTimeout() < 1) {
            hostEndpoint
                    .setReceiveTimeout(HostEndpoint.DEFAULT_RECEIVE_TIMEOUT_MSEC);
        }
        return hostEndpoint;
    }

    /**
     * Create a managed connection for a given LegStar transport.
     * 
     * @param hostConnection the LegStar transport connection
     * @param hostEndpoint the LegStar endpoint
     * @return a Managed Connection
     */
    public abstract ManagedConnection createManagedConnection(
            com.legstar.messaging.LegStarConnection hostConnection,
            HostEndpoint hostEndpoint);

    /**
     * Create a LegStar Transport endpoint.
     * 
     * @param subject the JAAS subject
     * @param connectionRequestInfo additional passed request information
     * @return a LegStar Transport endpoint
     */
    public abstract HostEndpoint createHostEndpoint(Subject subject,
            ConnectionRequestInfo connectionRequestInfo);

    public Object createConnectionFactory() throws ResourceException {
        if (log.isDebugEnabled()) {
            log.debug("Creating a new Connection Factory with default Connection Manager");
        }
        return createLegStarConnectionFactory(new LegStarDefaultConnectionManager());
    }

    public Object createConnectionFactory(ConnectionManager connectionManager)
            throws ResourceException {
        if (log.isDebugEnabled()) {
            log.debug("Creating a new Connection Factory for Connection Manager "
                    + connectionManager);
        }
        return createLegStarConnectionFactory(connectionManager);
    }

    /**
     * Create a connection factory for a specific transport.
     * 
     * @param connectionManager the connection manager
     * @return a new managed connection factory
     * @throws ResourceException if creation of factory fails
     */
    public abstract ConnectionFactory createLegStarConnectionFactory(
            ConnectionManager connectionManager) throws ResourceException;

    /**
     * The method matchManagedConnections enables application server to use a
     * resource adapter specific criteria for matching a ManagedConnection
     * instance to service a connection request.
     * 
     * @param connectionSet set of potential managed connections
     * @param subject the requestor (JAAS)
     * @param connectionRequestInfo
     * @return
     * @throws ResourceException
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public ManagedConnection matchManagedConnections(Set connectionSet,
            Subject subject, ConnectionRequestInfo connectionRequestInfo)
            throws ResourceException {
        if (log.isDebugEnabled()) {
            log.debug("Looking for Managed Connection matching subject: "
                    + LegStarUtil.toString(subject) + ", requestInfo: " + connectionRequestInfo
                    + " from " + connectionSet.size()
                    + " candidate managed connections");
        }

        Iterator < ManagedConnection > iterator = connectionSet.iterator();
        while (iterator.hasNext()) {
            LegStarManagedConnection candidate = (LegStarManagedConnection) iterator
                    .next();
            if (connectionRequestInfo != null) {
                if (match(candidate.getHostEndpoint(),
                        ((LegStarConnectionRequestInfo) connectionRequestInfo)
                                .getLegStarConnectionSpec())) {
                    if (log.isDebugEnabled()) {
                        log.debug("Managed Connection match found for requestInfo: "
                                + connectionRequestInfo);
                    }
                    return candidate;
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Managed Connection match found");
                }
                return candidate;
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("No Managed Connection match was found");
        }

        return null;
    }

    /**
     * Compare specs with current host endpoint parameters if they differ, then
     * the candidate is not suitable.
     * 
     * @param candidate the candidate host endpoint
     * @param specs the requested connection specifications
     * @return true if the host endpoint is compatible withy the specifications
     */
    private boolean match(HostEndpoint candidate, LegStarConnectionSpec specs) {
        if (specs == null) { // No specs, any connection will do
            return true;
        }
        if (specs.getHostUserID() != null) {
            if (!specs.getHostUserID().equals(candidate.getHostUserID())) {
                return false;
            }
        }
        if (specs.getHostPassword() != null) {
            if (!specs.getHostPassword().equals(candidate.getHostPassword())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Extract credentials from a subject attached to our
     * managedConnectionFactory.
     * <p/>
     * This needs to run as a privileged action.
     * 
     */
    private static class GetCredentialAction implements
            PrivilegedAction < PasswordCredential > {
        Subject subject;
        ManagedConnectionFactory mcf;

        GetCredentialAction(Subject subject, ManagedConnectionFactory mcf) {
            this.subject = subject;
            this.mcf = mcf;
        }

        public PasswordCredential run() {
            Set < PasswordCredential > pcs = subject
                    .getPrivateCredentials(PasswordCredential.class);
            Iterator < PasswordCredential > it = pcs.iterator();
            while (it.hasNext()) {
                PasswordCredential pc = it.next();
                if (pc.getManagedConnectionFactory().equals(mcf)) {
                    return pc;
                }
            }
            return null;
        }

    }

    public PrintWriter getLogWriter() throws ResourceException {
        return writer;
    }

    public void setLogWriter(PrintWriter printWriter) throws ResourceException {
        writer = printWriter;

    }

    public String getServerName() {
        return serverName;
    }

    public void setServerName(String serverName) {
        String oldServerName = getServerName();
        this.serverName = serverName;
        firePropertyChange("ServerName", oldServerName, serverName);
    }

    public Integer getPortNumber() {
        return portNumber;
    }

    public void setPortNumber(Integer portNumber) {
        int oldPortNumber = getPortNumber();
        this.portNumber = portNumber;
        firePropertyChange("PortNumber", oldPortNumber, portNumber);
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        String oldUserName = getUserName();
        this.userName = userName;
        firePropertyChange("UserName", oldUserName, userName);
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        String oldPassword = getPassword();
        this.password = password;
        firePropertyChange("Password", oldPassword, password);
    }

    public String getHostCharset() {
        return hostCharset;
    }

    public void setHostCharset(String hostCharset) {
        String oldHostCharset = getHostCharset();
        this.hostCharset = hostCharset;
        firePropertyChange("HostCharset", oldHostCharset, hostCharset);
    }

    public Boolean isHostTraceMode() {
        return hostTraceMode;
    }

    public void setHostTraceMode(Boolean hostTraceMode) {
        boolean oldHostTraceMode = isHostTraceMode();
        this.hostTraceMode = hostTraceMode;
        firePropertyChange("HostTraceMode", oldHostTraceMode, hostCharset);
    }

    public Integer getHostConnectTimeout() {
        return hostConnectTimeout;
    }

    public void setHostConnectTimeout(Integer hostConnectTimeout) {
        int oldHostConnectTimeout = getHostConnectTimeout();
        this.hostConnectTimeout = hostConnectTimeout;
        firePropertyChange("HostConnectTimeout", oldHostConnectTimeout,
                hostCharset);
    }

    public Integer getHostReceiveTimeout() {
        return hostReceiveTimeout;
    }

    public void setHostReceiveTimeout(Integer hostReceiveTimeout) {
        int oldHostReceiveTimeout = getHostReceiveTimeout();
        this.hostReceiveTimeout = hostReceiveTimeout;
        firePropertyChange("HostReceiveTimeout", oldHostReceiveTimeout,
                hostCharset);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((hostCharset == null) ? 0 : hostCharset.hashCode());
        result = prime * result
                + ((password == null) ? 0 : password.hashCode());
        result = prime * result + portNumber;
        result = prime * result
                + ((serverName == null) ? 0 : serverName.hashCode());
        result = prime * result
                + ((userName == null) ? 0 : userName.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        LegStarManagedConnectionFactoryImpl other = (LegStarManagedConnectionFactoryImpl) obj;
        if (hostCharset == null) {
            if (other.hostCharset != null) {
                return false;
            }
        } else if (!hostCharset.equals(other.hostCharset)) {
            return false;
        }
        if (password == null) {
            if (other.password != null) {
                return false;
            }
        } else if (!password.equals(other.password)) {
            return false;
        }
        if (portNumber != other.portNumber) {
            return false;
        }
        if (serverName == null) {
            if (other.serverName != null) {
                return false;
            }
        } else if (!serverName.equals(other.serverName)) {
            return false;
        }
        if (userName == null) {
            if (other.userName != null) {
                return false;
            }
        } else if (!userName.equals(other.userName)) {
            return false;
        }
        return true;
    }

}
